/*
 * 
 *  This file is part of Sourceprojects.org Lycia Copyright 2010 noctarius
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * 
 */
package org.sourceprojects.lycia.internal;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.sourceprojects.lycia.DefinitionBuilder;
import org.sourceprojects.lycia.EndTagAware;
import org.sourceprojects.lycia.LyciaConfigurator;
import org.sourceprojects.lycia.LyciaParser;
import org.sourceprojects.lycia.ParserContext;
import org.sourceprojects.lycia.ProgressListener;
import org.sourceprojects.lycia.QualifiedName;
import org.sourceprojects.lycia.ResourceResolver;
import org.sourceprojects.lycia.TagDefinitionParser;
import org.sourceprojects.lycia.annotations.AfterPropertySet;
import org.sourceprojects.lycia.exceptions.LyciaInitializationException;
import org.sourceprojects.lycia.exceptions.LyciaParsingException;
import org.sourceprojects.lycia.internal.bundled.ReaderInputStream;
import org.sourceprojects.lycia.internal.configuration.RankingXmlParserConfigurator;
import org.sourceprojects.lycia.internal.io.LyciaXmlReader;
import org.sourceprojects.lycia.internal.logging.LyciaLoggerWrapper;
import org.sourceprojects.lycia.io.LyciaReader;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;

/**
 * Standard implementation of the {@link LyciaParser} interface includes a set
 * of all basic features.
 * 
 * @author noctarius
 * @since 0.0.1
 * 
 * @param <B>
 *            Type of ContextObject
 */
public class AndroidCompatibleLyciaParserImpl<B> implements LyciaParser<B> {
	private static final LyciaLoggerWrapper LOGGER = LyciaLoggerWrapper
			.getLogger(AndroidCompatibleLyciaParserImpl.class);

	private final List<ProgressListener> listeners = new ArrayList<ProgressListener>();

	private LyciaConfigurator<B> configurator;
	private ResourceResolver resourceResolver;
	private ErrorHandler errorHandler;
	private ParserContext<B> parserContext;
	private Class<? extends LyciaReader<B>> readerClass;
	private LyciaReader<B> reader;
	private long expirationTime = 100;

	public AndroidCompatibleLyciaParserImpl() {
		this(null);
	}

	public AndroidCompatibleLyciaParserImpl(
			final LyciaConfigurator<B> configurator) {

		if (configurator != null) {
			this.configurator = configurator;
		} else {
			this.configurator = new RankingXmlParserConfigurator<B>(this, null);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.sourceprojects.lycia.XmlParser#addTagDefinitionParser(org.sourceprojects
	 * .lycia.TagDefinitionParser)
	 */
	@Override
	public void addTagDefinitionParser(
			final TagDefinitionParser<B> tagDefinitionParser) {

		configurator.addTagDefinitionParser(tagDefinitionParser);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.sourceprojects.lycia.XmlParser#addTagDefinitionParsers(org.sourceprojects
	 * .lycia.TagDefinitionParser<B>[])
	 */
	@Override
	public void addTagDefinitionParsers(
			final TagDefinitionParser<B>[] tagDefinitionParsers) {

		configurator.addTagDefinitionParsers(tagDefinitionParsers);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seeorg.sourceprojects.lycia.XmlParser#removeTagDefinitionParser(org.
	 * sourceprojects.lycia.TagDefinitionParser)
	 */
	@Override
	public void removeTagDefinitionParser(
			final TagDefinitionParser<B> tagDefinitionParser) {

		configurator.removeTagDefinitionParser(tagDefinitionParser);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seeorg.sourceprojects.lycia.XmlParser#removeTagDefinitionParsers(org.
	 * sourceprojects.lycia.TagDefinitionParser<B>[])
	 */
	@Override
	public void removeTagDefinitionParsers(
			final TagDefinitionParser<B>[] tagDefinitionParsers) {

		configurator.removeTagDefinitionParsers(tagDefinitionParsers);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#parse(java.io.InputStream)
	 */
	@Override
	public void parse(final InputStream inputStream)
			throws LyciaParsingException {

		try {
			// Create the per parser instances
			final Document document = createReader(inputStream);
			final DefinitionBuilder definitionBuilder = new DefinitionBuilderImpl(
					document);

			// Start parsing
			readNode(document.getFirstChild(), definitionBuilder);

		} catch (final Exception e) {
			throw new LyciaParsingException("Parsing failed", e);

		} finally {
			// Explicitly cleanup actual parserContext for threadded use
			if (parserContext instanceof InternalParserContext<?>) {
				final InternalParserContext<?> context = ((InternalParserContext<?>) parserContext);
				final Cache<?> patternCache = context.getContextUtils()
						.getPatternCache();
				final Cache<?> xpathCache = context.getContextUtils()
						.getXPathCache();

				if (patternCache != null) {
					patternCache.shutdown();
				}
				if (xpathCache != null) {
					xpathCache.shutdown();
				}

				context.setContextUtils(new ParserContextUtils());
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#parse(java.io.Reader)
	 */
	@Override
	public void parse(final Reader reader) throws LyciaParsingException {
		parse(reader, 0);
	}

	public void parse(final Reader reader, final long length)
			throws LyciaParsingException {

		parse(new ReaderInputStream(reader, length));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#parse(java.lang.String)
	 */
	@Override
	public void parse(final String xml) throws LyciaParsingException {
		parse(new DataLengthStringReader(xml, xml.length()));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#parse(java.io.File)
	 */
	@Override
	public void parse(final File file) throws LyciaParsingException {
		Reader reader = null;
		try {
			reader = new FileReader(file);
			parse(reader, file.length());
		} catch (final FileNotFoundException e) {
			throw new LyciaParsingException("Parsing failed", e);

		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (final IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#transform(java.io.InputStream)
	 */
	@Override
	public Document transform(final InputStream inputStream)
			throws LyciaParsingException {

		try {
			// Create the per parser instances
			final Document document = createReader(inputStream);

			// Create the output document
			final Document transformedDocument = createDocumentBuilder()
					.newDocument();

			final DefinitionBuilder definitionBuilder = new DefinitionBuilderImpl(
					transformedDocument);

			// Start transforming
			final Node result = transformNode(document.getFirstChild(),
					definitionBuilder);

			if (result != null) {
				transformedDocument.appendChild(result);
			}

			return transformedDocument;

		} catch (final Exception e) {
			throw new LyciaParsingException("Transforming failed", e);

		} finally {
			// Explicitly cleanup actual parserContext for threadded use
			if (parserContext instanceof InternalParserContext<?>) {
				((InternalParserContext<?>) parserContext)
						.setContextUtils(new ParserContextUtils());
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#transform(java.lang.String)
	 */
	@Override
	public Document transform(final String xml) throws LyciaParsingException {
		return transform(new DataLengthStringReader(xml, xml.length()));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#transform(java.io.Reader)
	 */
	@Override
	public Document transform(final Reader reader) throws LyciaParsingException {
		return transform(reader, 0);
	}

	public Document transform(final Reader reader, final long length)
			throws LyciaParsingException {
		return transform(new ReaderInputStream(reader, length));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#transform(java.io.File)
	 */
	@Override
	public Document transform(final File file) throws LyciaParsingException {
		Reader reader = null;
		try {
			reader = new FileReader(file);
			return transform(reader, file.length());
		} catch (final FileNotFoundException e) {
			throw new LyciaParsingException("Parsing failed", e);
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (final IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#getResourceResolver()
	 */
	@Override
	public ResourceResolver getResourceResolver() {
		return resourceResolver;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.sourceprojects.lycia.XmlParser#setResourceResolver(org.sourceprojects
	 * .lycia.ResourceResolver)
	 */
	@Override
	public void setResourceResolver(final ResourceResolver resourceResolver) {
		this.resourceResolver = resourceResolver;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#getErrorHandler()
	 */
	@Override
	public ErrorHandler getErrorHandler() {
		return errorHandler;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.sourceprojects.lycia.XmlParser#setErrorHandler(org.xml.sax.ErrorHandler
	 * )
	 */
	@Override
	public void setErrorHandler(final ErrorHandler errorHandler) {
		this.errorHandler = errorHandler;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#getParserContext()
	 */
	@Override
	public ParserContext<B> getParserContext() {
		return parserContext;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.sourceprojects.lycia.XmlParser#setParserContext(org.sourceprojects
	 * .lycia.ParserContext)
	 */
	@Override
	public void setParserContext(final ParserContext<B> parserContext) {
		this.parserContext = parserContext;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#isValidatingSchema()
	 */
	@Override
	public boolean isValidatingSchema() {
		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#setValidatingSchema(boolean)
	 */
	@Override
	public void setValidatingSchema(final boolean validatingSchema) {
		throw new UnsupportedOperationException(
				"Schema validating isn't available on Android platform");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.sourceprojects.lycia.XmlParser#addProgressListener(org.sourceprojects
	 * .lycia.ProgressListener)
	 */
	@Override
	public void addProgressListener(final ProgressListener listener) {
		listeners.add(listener);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.sourceprojects.lycia.XmlParser#removeProgressListener(org.sourceprojects
	 * .lycia.ProgressListener)
	 */
	@Override
	public void removeProgressListener(final ProgressListener listener) {
		listeners.remove(listener);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.sourceprojects.lycia.XmlParser#setConfigurator(org.sourceprojects
	 * .lycia.XmlParserConfigurator)
	 */
	@Override
	public void setConfigurator(final LyciaConfigurator<B> configurator) {
		this.configurator = configurator;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#getConfigurator()
	 */
	@Override
	public LyciaConfigurator<B> getConfigurator() {
		return configurator;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#getReader()
	 */
	@Override
	public Class<? extends LyciaReader<B>> getReader() {
		return readerClass;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#setReader(java.lang.Class)
	 */
	@Override
	public void setReader(final Class<? extends LyciaReader<B>> readerClass) {
		this.readerClass = readerClass;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#purge()
	 */
	@Override
	public void purge() {
		// On finish force caches to expire and cleanup
		if (parserContext instanceof InternalParserContext<?>) {
			final ParserContextUtils utils = ((InternalParserContext<?>) parserContext)
					.getContextUtils();

			utils.getPatternCache().shutdown();
			utils.getXPathCache().shutdown();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#getCacheExpirationTime()
	 */
	@Override
	public long getCacheExpirationTime() {
		return expirationTime;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.sourceprojects.lycia.XmlParser#setCacheExpirationTime(long)
	 */
	@Override
	public void setCacheExpirationTime(final long expirationTime) {
		this.expirationTime = expirationTime;
	}

	protected synchronized Document createReader(final InputStream inputStream)
			throws Exception {

		if (reader == null) {
			initializeReader();
		}

		final Document document = reader.read(new InputSource(
				new ProgressAwareReader(inputStream, listeners)));

		return document;
	}

	protected DocumentBuilder createDocumentBuilder() throws Exception {
		final DocumentBuilderFactory factory = DocumentBuilderFactory
				.newInstance();

		// setting options for clean parsing
		factory.setIgnoringComments(true);
		factory.setIgnoringElementContentWhitespace(true);
		factory.setNamespaceAware(true);

		return factory.newDocumentBuilder();
	}

	private void initializeReader() throws LyciaInitializationException {
		if (readerClass == null) {
			reader = new LyciaXmlReader<B>();
		} else {
			try {
				reader = readerClass.newInstance();
			} catch (final InstantiationException e) {
				throw new LyciaInitializationException(
						"Reader could not be initialized", e);
			} catch (final IllegalAccessException e) {
				throw new LyciaInitializationException(
						"Reader could not be initialized", e);
			}
		}

		reader.setXmlParser(this);

		for (final Method method : reader.getClass().getDeclaredMethods()) {
			final AfterPropertySet annotation = method
					.getAnnotation(AfterPropertySet.class);

			if (annotation != null) {
				if (!annotation.value()) {
					executeInitialization(method, reader,
							"Reader could not be initialized");
				} else {
					new Thread(createAsyncExecution(method, reader),
							"@AfterPropertySet executor").start();
				}
			}
		}
	}

	protected void readNode(final Node node,
			final DefinitionBuilder definitionBuilder)
			throws LyciaParsingException {

		// If node is an element
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			final String namespaceURI = node.getNamespaceURI();
			final String localName = node.getLocalName();

			// Try to resolve TagDefinitionParser
			final TagDefinitionParser<B> parser = findParserByNamespaceAndTag(
					namespaceURI, localName, (Element) node);

			if (parser != null) {
				final QualifiedName qualifiedName = new QualifiedName(
						namespaceURI, localName);

				parser.parseElement((Element) node, localName, qualifiedName,
						parserContext, definitionBuilder);
			}
		}

		// If node has children, go through all of them
		if (node.hasChildNodes()) {
			final NodeList children = node.getChildNodes();
			for (int i = 0; i < children.getLength(); i++) {
				readNode(children.item(i), definitionBuilder);
			}
		}

		// Inform registered EndTagListener
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			final String namespaceURI = node.getNamespaceURI();
			final String localName = node.getLocalName();

			for (EndTagAware<B> listener : getConfigurator()
					.getEndTagReachedListeners()) {

				listener.endTagReached((Element) node, localName, namespaceURI,
						parserContext);
			}
		}
	}

	protected Node transformNode(final Node node,
			final DefinitionBuilder definitionBuilder)
			throws LyciaParsingException {

		Node transformedNode = null;
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			final String namespaceURI = node.getNamespaceURI();
			final String localName = node.getLocalName();

			// Try to resolve TagDefinitionParser
			TagDefinitionParser<B> parser = null;
			if (namespaceURI != null) {
				parser = findParserByNamespaceAndTag(namespaceURI, localName,
						(Element) node);
			}

			if (parser != null) {
				final QualifiedName qualifiedName = new QualifiedName(
						namespaceURI, localName);

				transformedNode = parser.transformElement((Element) node,
						localName, qualifiedName, parserContext,
						definitionBuilder);

			} else {
				transformedNode = definitionBuilder.getDocument().importNode(
						node, false);
			}
		}

		// If node has children, go through all of them
		if (node.hasChildNodes()) {
			if (transformedNode != null) {
				final NodeList children = node.getChildNodes();
				for (int i = 0; i < children.getLength(); i++) {
					final Node result = transformNode(children.item(i),
							definitionBuilder);

					if (result != null) {
						transformedNode.appendChild(result);
					}
				}
			}
		}

		if (node.getNodeType() == Node.ELEMENT_NODE) {
			final String namespaceURI = node.getNamespaceURI();
			final String localName = node.getLocalName();

			for (EndTagAware<B> listener : getConfigurator()
					.getEndTagReachedListeners()) {

				listener.endTagReached((Element) node, localName, namespaceURI,
						parserContext);
			}
		}

		return transformedNode;
	}

	private TagDefinitionParser<B> findParserByNamespaceAndTag(
			final String namespaceURI, final String tagname,
			final Element element) {

		final TagDefinitionParser<B> result = configurator
				.findTagDefinitionParser(tagname, namespaceURI, element);

		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Found TagDefinitionParser '" + result + "' for tag '"
					+ namespaceURI + "->" + tagname);
		}

		return result;
	}

	protected List<ProgressListener> getListeners() {
		return listeners;
	}

	protected LyciaReader<B> getReaderInstance() {
		return reader;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "AndroidCompatibleLyciaParserImpl [configurator=" + configurator
				+ ", errorHandler=" + errorHandler + ", expirationTime="
				+ expirationTime + ", listeners=" + listeners
				+ ", parserContext=" + parserContext + ", reader=" + reader
				+ ", readerClass=" + readerClass + ", resourceResolver="
				+ resourceResolver + "]";
	}

	private Runnable createAsyncExecution(final Method method,
			final LyciaReader<B> reader) {
		return new Runnable() {
			@Override
			public void run() {
				executeInitialization(method, reader,
						"Asynchronous initialization failed");
			}
		};
	}

	private void executeInitialization(final Method method,
			final LyciaReader<B> reader, final String message) {
		try {
			method.invoke(reader);
		} catch (final IllegalArgumentException e) {
			throw new LyciaInitializationException(message, e);
		} catch (final IllegalAccessException e) {
			throw new LyciaInitializationException(message, e);
		} catch (final InvocationTargetException e) {
			throw new LyciaInitializationException(message, e);
		}
	}

}
